﻿using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using Newtonsoft.Json;
using Swimj.Core.Utilities;

namespace Swimj.Core.Extensions
{
    public static class ObjectExtensions
    {
        private static readonly MethodInfo CloneMethod = typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);

        public static bool IsIn<T>(this T item, params T[] list)
        {
            return list.Contains(item);
        }

        public static bool IsPrimitive(this Type type)
        {
            if (type == typeof(string))
            {
                return true;
            }

            return type.IsValueType & type.IsPrimitive;
        }

        public static Lazy<T> AsLazy<T>(this T t) where T : class
        {
            return new Lazy<T>(() => t, LazyThreadSafetyMode.PublicationOnly);
        }

        public static string ToQueryString(this object obj, string firstDelimiter = "")
        {
            var properties = from p in obj.GetType().GetProperties()
                             let value = GetValue(obj, p)
                             where value != null
                             select $"{GetKeyName(p)}={HttpUtility.UrlEncode(value.ToString())}";

            return $"/{firstDelimiter}?{string.Join("&", properties)}";
        }

        public static string? ToNullSafeString(this object? input, string defaultIfNull = "")
        {
            return input == null ? defaultIfNull : input.ToString();
        }

        public static IEnumerable<PropertyInfo> GetProperties(this object input, BindingFlags bindingAttr = default)
        {
            return GetProperties(input.GetType(), bindingAttr);
        }

        public static bool NotNull(this object input)
        {
            return !input.IsNull();
        }

        public static bool IsNull(this object? input)
        {
            return input == null;
        }

        public static Task<T> ToTask<T>(this T input)
        {
            return Task.FromResult(input);
        }

        public static T SetProperties<T>(this T instance, params Action<T>[] actions)
        {
            foreach (var action in actions)
                action(instance);
            return instance;
        }

        public static T? ExposeField<T>(this object instance, string fieldName)
        {
            return CheckHelper.TryCatch<T?, Exception>(() =>
            {
                BindingFlags flags = BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance |
                                     BindingFlags.NonPublic | BindingFlags.Public;
                var info = instance.GetType().GetProperty(fieldName, flags);
                if (info != null)
                    return (T?)info.GetValue(instance);
                return (T?)instance.GetType().GetField(fieldName, flags)?.GetValue(instance);
            });
        }

        private static string GetKeyName(MemberInfo p)
        {
            return (p.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? p.Name).ToLower();
        }

        private static object? GetValue(object obj, PropertyInfo p)
        {
            var res = p.GetValue(obj, null);
            if (res == null || p.PropertyType.IsValueType || p.PropertyType.IsAssignableFrom(typeof(string)))
            {
                return res;
            }

            return JsonConvert.SerializeObject(res);
        }


        public static Expression<Func<T, object?>> GetPropertyExpression<T>(this T instance, string propertyName)
        {
            var parameterExpression = Expression.Parameter(typeof(T), "i");
            var propertyExpression = Expression.Property(parameterExpression, typeof(T).GetProperty(propertyName)!);

            Expression<Func<T, object?>> expression = Expression.Lambda<Func<T, object?>>(
                propertyExpression, new ParameterExpression[]
                {
                    parameterExpression
                });
            return expression;
        }

        public static object DeepClone(this object originalObject)
        {
            return InternalCopy(originalObject, new Dictionary<object, object>(new ReferenceEqualityComparer()));
        }

        public static T DeepClone<T>(this T original)
        {
            return (T)DeepClone((object)original);
        }

        private static object InternalCopy(object originalObject, IDictionary<object, object> visited)
        {
            if (originalObject == null)
            {
                return null;
            }

            var typeToReflect = originalObject.GetType();
            if (IsPrimitive(typeToReflect))
            {
                return originalObject;
            }

            if (visited.ContainsKey(originalObject))
            {
                return visited[originalObject];
            }

            if (typeof(Delegate).IsAssignableFrom(typeToReflect))
            {
                return null;
            }

            var cloneObject = CloneMethod.Invoke(originalObject, null);
            if (typeToReflect.IsArray)
            {
                var arrayType = typeToReflect.GetElementType();
                if (!IsPrimitive(arrayType))
                {
                    Array clonedArray = (Array)cloneObject;
                    clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices));
                }
            }

            visited.Add(originalObject, cloneObject);
            CopyFields(originalObject, visited, cloneObject, typeToReflect);
            RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect);
            return cloneObject;
        }

        private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect)
        {
            if (typeToReflect.BaseType != null)
            {
                RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType);
                CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate);
            }
        }

        private static void CopyFields(object originalObject, IDictionary<object, object> visited,
            object cloneObject, Type typeToReflect,
            BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy, Func<FieldInfo, bool> filter = null)
        {
            foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags))
            {
                if (filter != null && !filter(fieldInfo))
                {
                    continue;
                }

                if (IsPrimitive(fieldInfo.FieldType) || fieldInfo.IsInitOnly)
                {
                    continue;
                }

                var originalFieldValue = fieldInfo.GetValue(originalObject);
                var clonedFieldValue = InternalCopy(originalFieldValue, visited);
                fieldInfo.SetValue(cloneObject, clonedFieldValue);
            }
        }
    }

    internal class ReferenceEqualityComparer : EqualityComparer<object>
    {
        public override bool Equals(object x, object y)
        {
            return ReferenceEquals(x, y);
        }

        public override int GetHashCode(object obj)
        {
            if (obj is null) return 0;
            return obj.GetHashCode();
        }
    }

    internal static class ArrayExtensions
    {
        public static void ForEach(this Array array, Action<Array, int[]> action)
        {
            if (array.LongLength == 0)
            {
                return;
            }

            ArrayTraverse walker = new ArrayTraverse(array);
            do action(array, walker.Position);
            while (walker.Step());
        }

        internal class ArrayTraverse
        {
            public int[] Position;
            private readonly int[] _maxLengths;

            public ArrayTraverse(Array array)
            {
                _maxLengths = new int[array.Rank];
                for (int i = 0; i < array.Rank; ++i)
                {
                    _maxLengths[i] = array.GetLength(i) - 1;
                }
                Position = new int[array.Rank];
            }

            public bool Step()
            {
                for (int i = 0; i < Position.Length; ++i)
                {
                    if (Position[i] < _maxLengths[i])
                    {
                        Position[i]++;
                        for (int j = 0; j < i; j++)
                        {
                            Position[j] = 0;
                        }
                        return true;
                    }
                }
                return false;
            }
        }
    }
}